if not MemeHUD then

  _G.MemeHUD = {}
  MemeHUD.mod_path = ModPath
  MemeHUD.buffers = {}
  MemeHUD.current_cam = nil
  MemeHUD.cam_forward = Vector3()
  
  if CustomPackageManager then
    local custom_assets = {}
    local check_assets = {
      textures = "texture",
      movies = "movie"
    }
    for dir, restype in pairs(check_assets) do
      for _, fname in pairs(file.GetFiles(MemeHUD.mod_path .. "assets/" .. dir)) do
        local m = fname:match("^(.*)." .. restype .. "$")
        if m then
          table.insert(custom_assets, {
            _meta = restype,
            path = dir .. "/" .. m
          })
        end
      end
    end
    CustomPackageManager:LoadPackageConfig(MemeHUD.mod_path .. "assets", custom_assets)
    MemeHUD.custom_assets = true
  end
  
  if XAudio then
    blt.xaudio.setup()
    local base_path = MemeHUD.mod_path .. "assets/sounds/"
    for _, dir in pairs(file.GetDirectories(base_path)) do
      local path = base_path .. dir .. "/"
      MemeHUD.buffers[dir] = {}
      local added = false
      for _, fname in pairs(file.GetFiles(path)) do
        if fname:match(".ogg$") then
          MemeHUD.buffers[dir][fname] = XAudio.Buffer:new(path .. fname)
          added = true
        end
      end
      if not added then
        MemeHUD.buffers[dir] = nil
      end
    end
  end

  MemeHUD.mappings = {}
  local file = io.open(MemeHUD.mod_path .. "assets/mappings.txt", "r")
  if file then
    MemeHUD.mappings = json.decode(file:read("*all"))
    file:close()
  end
  
  MemeHUD.texts = {}
  local file = io.open(MemeHUD.mod_path .. "assets/memes.txt", "r")
  if file then
    MemeHUD.texts = json.decode(file:read("*all"))
    file:close()
  end
  
  local screen_pos = Vector3()
  local world_pos = Vector3()
  local KillPop = class()
  function KillPop:init(position, is_special)
    self._panel = MemeHUD._panel:panel({
      visible = false
    })
    local damage_text = table.random(MemeHUD.texts) or "FAIL"
    damage_text = damage_text:gsub("<R>", tostring(math.random(1000)))
    self._hue = math.random(360)
    local text = self._panel:text({
      name = "text",
      text = math.random() < 0.5 and damage_text:lower() or damage_text,
      font = tweak_data.menu.pd2_massive_font,
      font_size = math.random(30, 50) + (is_special and 30 or 0),
      color = Color(1, hsv_to_rgb(self._hue, 1, 1)),
      blend_mode = "add"
    })
    local _, _, w, h = text:text_rect()
    self._panel:set_size(w, h)
    self._position = position
    self._lifetime = 3
    self._panel:animate(function ()
      self:world_animate()
      self:destroy()
    end)
  end

  function KillPop:world_animate()
    over(self._lifetime, function (p)
      if not MemeHUD.current_cam then
        return
      end
      mvector3.set(world_pos, self._position)
      mvector3.set(screen_pos, MemeHUD._ws:world_to_screen(MemeHUD.current_cam, world_pos))
      mvector3.subtract(world_pos, MemeHUD.current_cam:position())
      mvector3.normalize(world_pos)
      self._panel:set_center(screen_pos.x, screen_pos.y - 2 * self._panel:h() * (math.pow(math.min(p * 1.5, 1) - 1, 3) + 1))
      self._panel:set_alpha(2 * (1 - p))
      self._panel:set_visible(mvector3.dot(MemeHUD.cam_forward, world_pos) >= 0)
      self._panel:child("text"):set_color(Color(1, hsv_to_rgb(math.fmod(self._hue + p * 360 * self._lifetime, 360), 1, 1)))
    end)
  end

  function KillPop:destroy()
    if not alive(self._panel) or not alive(MemeHUD._panel) then
      return
    end
    --self._panel:stop()
    MemeHUD._panel:remove(self._panel)
  end
  
  local HitMarker = class(KillPop)
  function HitMarker:init(data)
    self._lifetime = data.lifetime or 2
    self._panel = MemeHUD._panel:panel({
      alpha = data.alpha,
      visible = false,
      layer = 10000
    })
    if data.background_color then
      self._panel:bitmap({
        texture = "units/white_df",
        color = Color(data.background_color),
        blend_mode = data.background_blend,
        w = self._panel:w(),
        h = self._panel:h(),
        layer = 0
      })
    end
    local texture = table.random(data.texture)
    if texture:match("^movies/") then
      if MemeHUD._video_playing then
        MemeHUD._video_playing:destroy()
      end
      MemeHUD._video_playing = self
      self._image = self._panel:video({
        video = texture,
        loop = true,
        color = data.color and Color(data.color),
        blend_mode = data.blend,
        layer = 1
      })
    else
      self._image = self._panel:bitmap({
        texture = texture,
        color = data.color and Color(data.color),
        blend_mode = data.blend,
        layer = 1
      })
      self._image:set_center(self._image:w() * 0.5 + math.random(self._panel:w() - self._image:w()), self._image:h() * 0.5 + math.random(self._panel:h() - self._image:h()))
    end
    local animation = data.animation and table.random(data.animation)
    if animation and self[animation] then
      self._panel:animate(function ()
        self[animation](self)
        self:destroy()
      end)
    else
      self._panel:animate(function ()
        wait(self._lifetime)
        self:destroy()
      end)
    end
    if alive(self._panel) then
      self._panel:set_visible(true)
      if data.pause then
        managers.time_speed:play_effect("pause", {
          speed = 0,
          fade_in = 0,
          fade_out = 0,
          sustain = self._lifetime,
          timer = "pausable",
          affect_timer = {
            "game",
            "player",
            "game_animation"
          }
        })
      end
    end
  end
  
  function HitMarker:grow()
    local w, h = self._image:w(), self._image:h()
    local x, y = self._image:center_x(), self._image:center_y()
    over(self._lifetime, function (p)
      local f = math.lerp(0.1, 4, p)
      self._image:set_size(w * f, h * f)
      self._panel:set_alpha(math.lerp(2, 0, p))
      self._image:set_center(x, y)
    end)
  end
  
  function HitMarker:fall()
    self._image:set_center(self._image:w() * 0.5 + math.random(self._panel:w() - self._image:w()), -math.random(self._panel:h()))
    local startrot = math.random(360)
    local rotspeed = math.random() * self._lifetime * 180
    local fallspeed = (1 + math.random()) * self._panel:h() * 2
    local x, y = self._image:x(), self._image:y()
    over(self._lifetime, function (p)
      self._image:set_rotation(startrot + p * rotspeed)
      self._image:set_center(x, y + p * fallspeed)
    end)
  end
  
  function HitMarker:shake()
    local x, y = self._image:center_x(), self._image:center_y()
    local last_p = 0
    over(self._lifetime, function (p)
      if p > last_p + 0.1 / self._lifetime then
        self._image:set_center(x + math.random(self._panel:w() / 8) * p, y + math.random(self._panel:h() / 8) * p)
      end
    end)
  end
  
  function HitMarker:fullscreen()
    self._image:set_position(0, 0)
    self._image:set_size(self._panel:w(), self._panel:h())
    self._image:set_align("stretch")
    self._image:set_valign("stretch")
    wait(self._lifetime)
  end
  
  function HitMarker:fullscreen_fade()
    self._image:set_position(0, 0)
    self._image:set_size(self._panel:w(), self._panel:h())
    self._image:set_align("stretch")
    self._image:set_valign("stretch")
    local alpha = self._panel:alpha()
    over(self._lifetime, function (p)
      self._panel:set_alpha(math.lerp(0, alpha, p <= 0.2 and p * 5 or p >= 0.8 and (1 - p) * 5 or 1))
    end)
  end
  
  function HitMarker:spin()
    local startrot = math.random(360)
    local x, y = self._image:center_x(), self._image:center_y()
    over(self._lifetime, function (p)
      self._image:set_rotation(startrot + math.round(p * 16 * self._lifetime) * 45)
      self._image:set_center(x, y)
    end)
  end
  
  function HitMarker:move()
    local x, y = self._image:center_x(), self._image:center_y()
    local left = math.random() < 0.5
    if left then
      self._image:set_center_x(-self._image:w())
    else
      self._image:set_texture_rect(self._image:w(), 0, -self._image:w(), self._image:h())
      self._image:set_center_x(self._panel:w() + self._image:w())
    end
    local ymove = math.lerp(-2, 2, math.random())
    over(self._lifetime, function (p)
      y = y + ymove
      self._image:set_center(left and p * (self._panel:w() + self._image:w() * 2) or (1 - p) * (self._panel:w() + self._image:w() * 2) - self._image:w(), y + math.sin(p * 720) * 64)
    end)
  end
  
  function HitMarker:animate()
    local w, h = self._image:w(), self._image:h()
    local frames = w / h
    local frame = 0
    local fps = 4
    self._image:set_texture_rect(0, 0, h, h)
    self._image:set_size(h, h)
    self._image:set_center(self._image:w() * 0.5 + math.random(self._panel:w() - self._image:w()), self._image:h() * 0.5 + math.random(self._panel:h() - self._image:h()))
    over(self._lifetime, function (p)
      frame = math.floor(p * fps * self._lifetime) % frames
      self._image:set_texture_rect(frame * h, 0, h, h)
    end)
  end
  
  function HitMarker:destroy()
    self.super.destroy(self)
    if MemeHUD._video_playing == self then
      MemeHUD._video_playing = nil
    end
  end

  function MemeHUD:play_sound(id, chance, blocking)
    local can_play = not blocking or not self._last_sound_t or self._t > self._last_sound_t + blocking
    if can_play and self.buffers[id] and math.random() < (chance or 1) then
      local random_key = table.random_key(self.buffers[id])
      local buffer = self.buffers[id][random_key]
      if not buffer then
        return
      end
      local src = XAudio.Source:new(buffer)
      src:set_relative(true)
      src._gain = id == "hit" and 2 or 1.25
      self._last_sound_t = blocking and self._t or self._last_sound_t
      
      local mapping = self.mappings[random_key]
      if mapping and MemeHUD.custom_assets then
        mapping = table.random(mapping)
        local num = math.random(mapping.min or 1, mapping.max or 1)
        for i = 1, num do
          self:add_hit_marker(mapping)
        end
      end
      
    end
  end
  
  function MemeHUD:add_kill_pop(unit, info)
    local col_ray = info.col_ray or {}
    local unit_base = unit:base()
    local is_special = unit_base._tweak_table and tweak_data.character[unit_base._tweak_table] and tweak_data.character[unit_base._tweak_table].priority_shout
    KillPop:new(col_ray.position or info.pos or col_ray.hit_position or unit:position(), is_special)
    local chance = math.min(0.5, not self._last_sound_t and 1 or (self._t - self._last_sound_t) / 10)
    self:play_sound("kill", managers.groupai:state():whisper_mode() and 1 or chance, 2)
  end
  
  function MemeHUD:add_hit_marker(data)
    HitMarker:new(data)
  end
  
  function MemeHUD:init()
    local hud = managers.hud:script(PlayerBase.PLAYER_INFO_HUD_FULLSCREEN_PD2)
    self._ws = managers.hud._workspace
    self._panel = self._panel or hud and hud.panel or self._ws:panel({ name = "MemeHUD" })
  end

  function MemeHUD:update(t, dt)
    self._t = t
    self.current_cam = managers.viewport:get_current_camera()
    if not self.current_cam then
      return
    end
    mrotation.y(self.current_cam:rotation(), self.cam_forward)
  end

end

if RequiredScript == "lib/managers/hudmanager" then

  local init_finalize_original = HUDManager.init_finalize
  function HUDManager:init_finalize(...)
    local result = init_finalize_original(self, ...)
    MemeHUD:init()
  end

  local update_original = HUDManager.update
  function HUDManager:update(...)
    update_original(self, ...)
    MemeHUD:update(...)
  end

end

if RequiredScript == "lib/managers/hud/hudblackscreen" then

  local init_original = HUDBlackScreen.init
  function HUDBlackScreen:init(hud, ...)
    init_original(self, hud, ...)
    
    self._memehud_initialized = self._blackscreen_panel:text({
      y = 100,
      vertical = "center",
      align = "center",
      text = "MEMEHUD V" ..  (1 + math.random()) .. " INITIALIZED",
      alpha = 0,
      visible = true,
      layer = 1,
      color = Color.white,
      valign = {
          0.8,
          1
      },
      font_size = tweak_data.hud.default_font_size,
      font = tweak_data.hud.medium_font,
      w = self._blackscreen_panel:w()
    })
    local _, _, _, h1 = self._memehud_initialized:text_rect()

    self._memehud_initialized:set_h(h1)
    self._memehud_initialized:set_center_x(self._blackscreen_panel:center_x())
    self._memehud_initialized:set_center_y(self._blackscreen_panel:h() / 2.9)
  end

  local fade_in_mid_text_original = HUDBlackScreen.fade_in_mid_text
  function HUDBlackScreen:fade_in_mid_text(...)
    if XAudio then
      local src = XAudio.Source:new(XAudio.Buffer:new(MemeHUD.mod_path .. "assets/sounds/initialized.ogg"))
      if src then
        src:set_relative(true)
        src._gain = 4
      end
    end
  
    self._memehud_initialized:animate(callback(self, self, "_animate_fade_in"))
    return fade_in_mid_text_original(self, ...)
  end
  
  local fade_out_mid_text_original = HUDBlackScreen.fade_out_mid_text
  function HUDBlackScreen:fade_out_mid_text(...)
    self._memehud_initialized:animate(callback(self, self, "_animate_fade_out"))
    return fade_out_mid_text_original(self, ...)
  end

end

if RequiredScript == "lib/units/beings/player/states/playerstandard" then
  
  local _start_action_throw_grenade_original = PlayerStandard._start_action_throw_grenade
  function PlayerStandard:_start_action_throw_grenade(...)
    MemeHUD:play_sound("grenade", 1, 1)
    return _start_action_throw_grenade_original(self, ...)
  end

end

if RequiredScript == "lib/units/beings/player/playerdamage" then
  
  local on_downed_original = PlayerDamage.on_downed
  function PlayerDamage:on_downed(...)
    MemeHUD:play_sound("downed", 1, 1)
    return on_downed_original(self, ...)
  end

end

if RequiredScript == "lib/units/enemies/cop/copdamage" then
  
  local _call_listeners_original = CopDamage._call_listeners
  function CopDamage:_call_listeners(damage_info, ...)
    if damage_info.attacker_unit and damage_info.attacker_unit == managers.player:local_player() then
      if self._dead then
        MemeHUD:add_kill_pop(self._unit, damage_info)
      end
      MemeHUD:play_sound("hit")
    end
    return _call_listeners_original(self, damage_info, ...)
  end

end
